/*!	 trgt_yuv.cpp
**	 Template File
**
**	Copyright (c) 2002-2005 Robert B. Quattlebaum Jr., Adrian Bentley
**	Copyright (c) 2007 Chris Moore
**
**	This package is free software; you can redistribute it and/or
**	modify it under the terms of the GNU General Public License as
**	published by the Free Software Foundation; either version 2 of
**	the License, or (at your option) any later version.
**
**	This package is distributed in the hope that it will be useful,
**	but WITHOUT ANY WARRANTY; without even the implied warranty of
**	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
**	General Public License for more details.
**
*/

#ifdef USING_PCH
#	include "pch.h"
#else
#ifdef HAVE_CONFIG_H
#	include <config.h>
#endif

#include "trgt_yuv.h"
#include <ETL/stringf>
#include <cstdio>
#include <algorithm>
#include <functional>
#endif

using namespace synfig;
using namespace std;
using namespace etl;

#define Y_FLOOR	(16)
#define Y_CEIL	(235)
#define Y_RANGE (Y_CEIL-Y_FLOOR)

#define UV_FLOOR	(16)
#define UV_CEIL		(240)
#define UV_RANGE (UV_CEIL-UV_FLOOR)

SYNFIG_TARGET_INIT(yuv);
SYNFIG_TARGET_SET_NAME(yuv, "yuv420p");
SYNFIG_TARGET_SET_EXT(yuv, "yuv");
SYNFIG_TARGET_SET_VERSION(yuv, "0.1");
SYNFIG_TARGET_SET_CVS_ID(yuv, "$Id$");

yuv::yuv(const char *FILENAME, const synfig::TargetParam& /* params */):
    filename(FILENAME),
    file((filename == "-") ? stdout : fopen(filename.c_str(), POPEN_BINARY_WRITE_TYPE)),
    dithering(true)
{
    // YUV420P doesn't have an alpha channel
    set_alpha_mode(TARGET_ALPHA_MODE_FILL);
}

yuv::~yuv()
{
}

bool
yuv::init(synfig::ProgressCallback * /* cb */)
{
    if (!file) {
        return false;
    }

    fprintf(file.get(), "YUV4MPEG2 W%d H%d F%d:1 Ip\n",
            desc.get_w(), desc.get_h(),
            round_to_int(desc.get_frame_rate()));
    return true;
}

bool
yuv::set_rend_desc(RendDesc *given_desc)
{
    given_desc->clear_flags();

    // Make sure our width is divisible by two
    given_desc->set_w(given_desc->get_w() * 2 / 2);
    given_desc->set_h(given_desc->get_h() * 2 / 2);

    desc = *given_desc;

    // Set up our surface
    surface.set_wh(desc.get_w(), desc.get_h());

    return true;
}

bool
yuv::start_frame(synfig::ProgressCallback */*callback*/)
{
    fprintf(file.get(), "FRAME\n");
    return static_cast<bool>(file);
}

Color *
yuv::start_scanline(int x)
{
    return surface[x];
}

bool
yuv::end_scanline()
{
    return static_cast<bool>(file);
}

void
yuv::end_frame()
{
    const int w = desc.get_w(), h = desc.get_h();
    int x, y;

    assert(file);

    // Output Y' channel, adjusting
    // the gamma as we go
    for (y = 0; y < h; y++)
        for (x = 0; x < w; x++) {
            Color& c(surface[y][x]);
            c = c.clamped();
            c.set_r(gamma().r_F32_to_F32(c.get_r()));
            c.set_g(gamma().g_F32_to_F32(c.get_g()));
            c.set_b(gamma().b_F32_to_F32(c.get_b()));
            float f(c.get_y());
            int i(max(min(round_to_int(c.get_y()*Y_RANGE), Y_RANGE), 0) + Y_FLOOR);

            if (dithering) {
                const float er(f - ((float)i - Y_FLOOR) / Y_RANGE);
                const Color error(er, er, er);

                if (surface.get_h() > y + 1) {
                    surface[y + 1][x - 1] += error * ((float)3 / (float)16);
                    surface[y + 1][x] += error * ((float)5 / (float)16);

                    if (surface.get_w() > x + 1) {
                        surface[y + 1][x + 1] += error * ((float)1 / (float)16);
                    }
                }

                if (surface.get_w() > x + 1) {
                    surface[y][x + 1] += error * ((float)7 / (float)16);
                }
            }

            fputc(i, file.get());
        }

    // Create new super-sampled surface
    Surface sm_surface(w / 2, h / 2);

    for (y = 0; y < h; y += 2)
        for (x = 0; x < w; x += 2) {
            Color c(Color::alpha());
            c += surface[y][x];
            c += surface[y + 1][x];
            c += surface[y][x + 1];
            c += surface[y + 1][x + 1];
            c /= 4;
            sm_surface[y / 2][x / 2] = c;
        }

    // Output U channel
    for (y = 0; y < sm_surface.get_h(); y++)
        for (x = 0; x < sm_surface.get_w(); x++) {
            const Color& c(sm_surface[y][x]);
            const float f(c.get_u());
            const int i(max(min(round_to_int((f + 0.5f)*UV_RANGE), UV_RANGE), 0) + UV_FLOOR);

            if (dithering) {
                const float er(f - ((((float)i - UV_FLOOR) / UV_RANGE) - 0.5f));
                const Color error(Color::YUV(0, er, 0));

                if (sm_surface.get_h() > y + 1) {
                    sm_surface[y + 1][x - 1] += error * ((float)3 / (float)16);
                    sm_surface[y + 1][x] += error * ((float)5 / (float)16);

                    if (sm_surface.get_w() > x + 1) {
                        sm_surface[y + 1][x + 1] += error * ((float)1 / (float)16);
                    }
                }

                if (sm_surface.get_w() > x + 1) {
                    sm_surface[y][x + 1] += error * ((float)7 / (float)16);
                }
            }

            fputc(i, file.get());
        }

    // Output V channel
    for (y = 0; y < sm_surface.get_h(); y++)
        for (x = 0; x < sm_surface.get_w(); x++) {
            const Color& c(sm_surface[y][x]);
            const float f(c.get_v());
            const int i(max(min(round_to_int((f + 0.5f)*UV_RANGE), UV_RANGE), 0) + UV_FLOOR);

            if (dithering) {
                const float er(f - ((((float)i - UV_FLOOR) / UV_RANGE) - 0.5f));
                const Color error(Color::YUV(0, 0, er));

                if (sm_surface.get_h() > y + 1) {
                    sm_surface[y + 1][x - 1] += error * ((float)3 / (float)16);
                    sm_surface[y + 1][x] += error * ((float)5 / (float)16);

                    if (sm_surface.get_w() > x + 1) {
                        sm_surface[y + 1][x + 1] += error * ((float)1 / (float)16);
                    }
                }

                if (sm_surface.get_w() > x + 1) {
                    sm_surface[y][x + 1] += error * ((float)7 / (float)16);
                }
            }

            fputc(i, file.get());
        }

    // Flush out the frame
    fflush(file.get());
}